TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { notFound } from "next/navigation";3import { ArrowLeft } from "lucide-react";4import { requireAdmin } from "@/lib/session";5import { getOrganizationDetail } from "@/lib/queries/admin";6import { formatBytes, formatDate, formatMs, formatNumber, formatPercent, formatUsd } from "@/lib/format";7import { PageHeader } from "@/components/ui/page-header";8import { Badge, StatusBadge } from "@/components/ui/badge";9import { Button } from "@/components/ui/button";10import { Stat, StatGrid } from "@/components/ui/stat";11import { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow } from "@/components/ui/table";12import { CopyButton } from "@/components/ui/copy-button";13import { Alert } from "@/components/ui/alert";14import { DateCell, KV, MarginText, Mono, NetworkBadge, Panel, PlanBadge, numCell } from "@/components/admin/primitives";15import { OrgLimitsForm, OrgProviderVisibilitySwitch, OrgSuspendSwitch } from "@/components/admin/org-actions";1617export const dynamic = "force-dynamic";1819export default async function AdminOrganizationDetail({ params }: { params: Promise<{ id: string }> }) {20 await requireAdmin();21 const { id } = await params;22 const d = await getOrganizationDetail(id);23 if (!d) notFound();24 const { org, stats30d: s } = d;25 const margin = s.revenue - s.cost;2627 return (28 <>29 <div className="mb-4">30 <Button asChild variant="ghost" size="sm" className="-ml-2">31 <Link href="/admin/organizations">32 <ArrowLeft className="size-3.5" /> Organizations33 </Link>34 </Button>35 </div>36 <PageHeader37 eyebrow="Organization"38 title={39 <span className="flex flex-wrap items-center gap-2">40 {org.name}41 <PlanBadge plan={org.plan} />42 {org.suspended ? <StatusBadge status="suspended" /> : null}43 {org.providerVisibility ? <Badge variant="warning" dot>provider debug on</Badge> : null}44 </span>45 }46 description={47 <span className="inline-flex items-center gap-1">48 <Mono>{org.id}</Mono>49 <CopyButton value={org.id} size="icon-sm" className="size-6" />50 <span className="text-fg-subtle">· /{org.slug}</span>51 </span>52 }53 actions={54 <Button asChild variant="outline" size="sm">55 <Link href={`/admin/requests?org=${org.id}`}>Explore requests</Link>56 </Button>57 }58 />5960 {org.suspended ? <Alert variant="danger" title="Suspended" className="mb-4">API access is blocked for this organization.</Alert> : null}6162 <StatGrid cols={6} className="mb-6">63 <Stat label="Requests 30d" value={formatNumber(s.requests)} />64 <Stat label="Success" value={formatPercent(s.requests ? (s.successes / s.requests) * 100 : null)} />65 <Stat label="Revenue 30d" value={formatUsd(s.revenue)} />66 <Stat label="Upstream cost" value={formatUsd(s.cost)} />67 <Stat label="Margin" value={<MarginText value={margin} pct={s.revenue > 0 ? (margin / s.revenue) * 100 : null} />} />68 <Stat label="Bandwidth" value={formatBytes(s.bytes)} />69 </StatGrid>7071 <div className="grid gap-6 lg:grid-cols-2">72 <Panel title="Controls" description="Every change is written to the audit log with your user id.">73 <div className="grid gap-5">74 <div className="grid gap-1.5">75 <div className="text-[13px] font-medium">Plan</div>76 <div className="flex items-center gap-2">77 <Badge variant="solid">Unlimited</Badge>78 <span className="text-[12.5px] text-fg-muted">private platform — plans are not configurable</span>79 </div>80 </div>81 <OrgProviderVisibilitySwitch orgId={org.id} enabled={org.providerVisibility} />82 <OrgSuspendSwitch orgId={org.id} suspended={org.suspended} />83 </div>84 </Panel>85 <div className="grid gap-6">86 <Panel title="Spend limits">87 <OrgLimitsForm orgId={org.id} softLimitUsd={org.softLimitUsd} hardLimitUsd={org.hardLimitUsd} />88 </Panel>89 <Panel title="Details">90 <KV91 items={[92 { label: "Owner", value: d.owner ? <Link href={`/admin/users/${d.owner.id}`} className="hover:underline">{d.owner.email}</Link> : "—" },93 { label: "Stripe customer", value: org.stripeCustomerId ?? "not linked", mono: true },94 { label: "Created", value: formatDate(org.createdAt) },95 { label: "Updated", value: formatDate(org.updatedAt) },96 ]}97 />98 </Panel>99 </div>100 </div>101102 <Panel title="Members" flush className="mt-6">103 <Table>104 <TableHeader>105 <TableRow>106 <TableHead>User</TableHead>107 <TableHead>Role</TableHead>108 <TableHead className="text-right">Joined</TableHead>109 </TableRow>110 </TableHeader>111 <TableBody>112 {d.members.map((m) => (113 <TableRow key={m.userId}>114 <TableCell>115 <Link href={`/admin/users/${m.userId}`} className="font-medium hover:underline">116 {m.email}117 </Link>118 <span className="ml-2 text-fg-muted">{m.name}</span>119 </TableCell>120 <TableCell className="text-fg-muted">{m.role}</TableCell>121 <TableCell className="text-right">122 <DateCell value={m.joinedAt} />123 </TableCell>124 </TableRow>125 ))}126 </TableBody>127 </Table>128 </Panel>129130 <div className="mt-6 grid gap-6 xl:grid-cols-2">131 <Panel title="Projects" flush>132 <Table>133 <TableHeader>134 <TableRow>135 <TableHead>Project</TableHead>136 <TableHead>Env</TableHead>137 <TableHead className="text-right">Keys</TableHead>138 <TableHead className="text-right">Req 30d</TableHead>139 <TableHead className="text-right">Created</TableHead>140 </TableRow>141 </TableHeader>142 <TableBody>143 {d.projects.length === 0 ? (144 <TableEmpty colSpan={5}>No projects.</TableEmpty>145 ) : (146 d.projects.map((p) => (147 <TableRow key={p.id}>148 <TableCell>149 <Link href={`/admin/requests?project=${p.id}`} className="font-medium hover:underline">150 {p.name}151 </Link>152 {p.archivedAt ? <Badge variant="outline" className="ml-2">archived</Badge> : null}153 <div className="font-mono text-[11.5px] text-fg-subtle">{p.id}</div>154 </TableCell>155 <TableCell>156 <Badge variant={p.environment === "development" ? "warning" : "success"} dot>157 {p.environment}158 </Badge>159 </TableCell>160 <TableCell className={numCell}>{p.keys}</TableCell>161 <TableCell className={numCell}>{formatNumber(p.requests30d)}</TableCell>162 <TableCell className="text-right">163 <DateCell value={p.createdAt} />164 </TableCell>165 </TableRow>166 ))167 )}168 </TableBody>169 </Table>170 </Panel>171 <Panel title="API keys" description="Prefix and last 4 only. Plaintext keys are never stored." flush>172 <Table>173 <TableHeader>174 <TableRow>175 <TableHead>Key</TableHead>176 <TableHead>Project</TableHead>177 <TableHead>Mode</TableHead>178 <TableHead>Status</TableHead>179 <TableHead className="text-right">Last used</TableHead>180 </TableRow>181 </TableHeader>182 <TableBody>183 {d.keys.length === 0 ? (184 <TableEmpty colSpan={5}>No API keys.</TableEmpty>185 ) : (186 d.keys.map((k) => (187 <TableRow key={k.id}>188 <TableCell>189 <div className="font-medium">{k.name}</div>190 <Mono className="text-fg-muted">191 {k.keyPrefix}…{k.last4}192 </Mono>193 </TableCell>194 <TableCell className="text-fg-muted">{k.projectName ?? "—"}</TableCell>195 <TableCell>196 <StatusBadge status={k.mode} />197 </TableCell>198 <TableCell>{k.revokedAt ? <StatusBadge status="revoked" /> : k.expiresAt && k.expiresAt < new Date() ? <StatusBadge status="expired" /> : <StatusBadge status="active" />}</TableCell>199 <TableCell className="text-right">200 <DateCell value={k.lastUsedAt} />201 </TableCell>202 </TableRow>203 ))204 )}205 </TableBody>206 </Table>207 </Panel>208 </div>209210 <Panel title="Recent requests" description="Latest 25. Cost is upstream; price is what the customer is billed." flush className="mt-6">211 <Table>212 <TableHeader>213 <TableRow>214 <TableHead>Request</TableHead>215 <TableHead>Domain</TableHead>216 <TableHead>Status</TableHead>217 <TableHead>Network</TableHead>218 <TableHead>Country</TableHead>219 <TableHead className="text-right">Latency</TableHead>220 <TableHead className="text-right">Cost</TableHead>221 <TableHead className="text-right">Price</TableHead>222 <TableHead className="text-right">When</TableHead>223 </TableRow>224 </TableHeader>225 <TableBody>226 {d.recentRequests.length === 0 ? (227 <TableEmpty colSpan={9}>No requests yet.</TableEmpty>228 ) : (229 d.recentRequests.map((r) => (230 <TableRow key={r.id}>231 <TableCell>232 <Link href={`/admin/requests/${r.id}`} className="hover:underline">233 <Mono>{r.id}</Mono>234 </Link>235 </TableCell>236 <TableCell className="max-w-[220px] truncate">{r.domain}</TableCell>237 <TableCell>238 <StatusBadge status={r.status} />239 {r.errorCode ? <span className="ml-1.5 font-mono text-[11.5px] text-fg-muted">{r.errorCode}</span> : null}240 </TableCell>241 <TableCell>242 <NetworkBadge network={r.network} />243 </TableCell>244 <TableCell>245 <Mono>{r.country ?? "—"}</Mono>246 </TableCell>247 <TableCell className={numCell}>{formatMs(r.latencyMs)}</TableCell>248 <TableCell className={numCell}>{formatUsd(r.costUsd, true)}</TableCell>249 <TableCell className={numCell}>{formatUsd(r.priceUsd, true)}</TableCell>250 <TableCell className="text-right">251 <DateCell value={r.createdAt} />252 </TableCell>253 </TableRow>254 ))255 )}256 </TableBody>257 </Table>258 </Panel>259260 <Panel title="Audit log" description="Last 30 entries scoped to this organization." flush className="mt-6">261 <Table>262 <TableHeader>263 <TableRow>264 <TableHead>When</TableHead>265 <TableHead>Action</TableHead>266 <TableHead>Target</TableHead>267 <TableHead>Metadata</TableHead>268 </TableRow>269 </TableHeader>270 <TableBody>271 {d.audit.length === 0 ? (272 <TableEmpty colSpan={4}>No audit entries.</TableEmpty>273 ) : (274 d.audit.map((a) => (275 <TableRow key={a.id}>276 <TableCell>277 <DateCell value={a.createdAt} relative={false} />278 </TableCell>279 <TableCell>280 <Badge variant={a.action === "admin.action" ? "danger" : "default"}>{a.action}</Badge>281 </TableCell>282 <TableCell>283 <Mono>{a.target ?? "—"}</Mono>284 </TableCell>285 <TableCell className="max-w-[420px] truncate font-mono text-[12px] text-fg-muted" title={a.metadata ? JSON.stringify(a.metadata) : undefined}>286 {a.metadata ? JSON.stringify(a.metadata) : "—"}287 </TableCell>288 </TableRow>289 ))290 )}291 </TableBody>292 </Table>293 </Panel>294 </>295 );296}297